package fregat.connector.base;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import fregat.connector.ConnectorService;
import fregat.base.net.DefaultProtocol;
import fregat.base.util.Logger;

/**
 * Static protocol class for sending and receiving ASCII data.
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0
 */
public class ASCIIProtocol extends DefaultProtocol {

  private int fixedLength; // size of fixed length telegrams
  private int headerLength; // size of header
  private int lengthPosition; // position of telegram length in header
  private int lengthDigits; // number of digits of telegram length in header
  private boolean exclusiveHeader; // flag whether telegram length excludes header
  private Delimiter startDelimiter; // start delimiter
  private Delimiter endDelimiter; // end delimiter
  private char filler; // filler character

  /**
   * Creates a new default protocol with the given name.
   *
   * @param name Name of instance that uses the protocol
   */
  public ASCIIProtocol(String name) {
    super(name);
  }

  /**
   * Receives data from the input reader.
   *
   * @param in Input stream
   * @return Received data(including header)
   * @throws IOException
   */
  public byte[] receive(InputStream in) throws IOException {
    String receivedData = null;

    DataInputStream stream;
    if (in instanceof DataInputStream) {
      stream = (DataInputStream) in;
    } else {
      stream = new DataInputStream(in);
    }

    // read to next start delimiter (if configured)
    if (startDelimiter != null) {
      readDelimiter(stream, startDelimiter);
    }

    if (fixedLength > 0) {
      // fixed length - read telegram at once
      byte[] data = new byte[fixedLength];
      stream.readFully(data);

      receivedData = new String(data);

    } else if (headerLength > 0) {
      // read header
      byte[] header = new byte[headerLength];
      stream.readFully(header);
      String headerData = new String(header);

      try {
        // get telegram length
        int length = Integer.parseInt(headerData.substring(lengthPosition, lengthPosition + lengthDigits));

        if (!exclusiveHeader) {
          length -= headerLength;
        }

        // read rest of telegram
        byte[] data = new byte[length];
        stream.readFully(data);

        // merge header and data
        receivedData = headerData + new String(data);

      } catch (NumberFormatException ex) {
        String debug = getName() + ", " + ConnectorService.resource.getString("ASCIIProtocol_receive_2")
          .replaceFirst("%1", headerData);
        Logger.getDefaultCategory().error(debug);
      }
    } else if (endDelimiter != null) {
      // read till end delimiter
      String data = readDelimiter(stream, endDelimiter);

      // extract data
      if (data != null && !endDelimiter.included) {
        receivedData = data.substring(0, data.length() - endDelimiter.characters.length());
      } else {
        receivedData = data;
      }
    } else {
      // read to next line feed and/or carriage return
      byte[] data = super.receive(stream);
      if (data != null) {
        receivedData = new String(data);
      }
    }

    // append start delimiter (if configured)
    if (startDelimiter != null && startDelimiter.included && receivedData != null) {
      receivedData = startDelimiter.characters + receivedData;
    }

    // convert to byte to match interface
    if (receivedData != null) {
      return receivedData.getBytes();
    } else {
      return null;
    }
  }

  /**
   * Sends the given data to the output stream. If no header or fixed length is
   * used the data is followed by a carriage return.
   *
   * @param out      Output stream
   * @param sendData Data to be sent
   * @return True, if send was success
   */
  public boolean send(OutputStream out, byte[] sendData) {
    try {
      // convert to print stream
      PrintStream stream;
      if (out instanceof PrintStream) {
        stream = (PrintStream) out;
      } else {
        stream = new PrintStream(out);
      }

      StringBuffer buffer = new StringBuffer();
      int length = fixedLength;

      if (startDelimiter != null && !startDelimiter.included) {
        // write start characters
        buffer.append(startDelimiter.characters);
        length += startDelimiter.characters.length();
      }

      buffer.append(new String(sendData));

      if (fixedLength == 0 && headerLength == 0 && endDelimiter == null) {
        ConnectorService.note(getName() + ", " + ConnectorService.resource.getString("ASCIIProtocol_send_1") +
          " '" + buffer.toString() + "'");
        return super.send(stream, buffer.toString().getBytes());
      }

      if (endDelimiter != null && !endDelimiter.included) {
        // write end characters
        buffer.append(endDelimiter.characters);
      }

      // append filler characters
      while (fixedLength > 0 && buffer.length() < length) {
        buffer.append(filler);
      }

      ConnectorService.note(getName() + ", " + ConnectorService.resource.getString("ASCIIProtocol_send_1") +
        " '" + buffer.toString() + "'");
      stream.write(buffer.toString().getBytes());

      return !stream.checkError();

    } catch (IOException ex) {
      Logger.getDefaultCategory().error("TCPProtocol.send(...)", ex);
      return false;
    }
  }

  /**
   * Sets the telegram length, if telegrams with fixed length are used.
   *
   * @param fixedLength Length of telegrams
   * @param filler      Filler character
   */
  public void setFixedLength(int fixedLength, String filler) {
    this.fixedLength = fixedLength;

    if (filler.startsWith("[")) {
      this.filler = (char) Integer.parseInt(filler.substring(1, filler.length() - 1));
    } else {
      this.filler = filler.charAt(0);
    }
  }

  /**
   * Sets the telegram header data, if header with telegram length field is used.
   *
   * @param headerLength    Total length of header
   * @param lengthPosition  Position of telegram length in header
   * @param lengthDigits    Number of digits of telegram lengths
   * @param exclusiveHeader True, if telegram length is without the header's length
   */
  public void setHeader(int headerLength, int lengthPosition,
                        int lengthDigits, boolean exclusiveHeader) {
    this.headerLength = headerLength;
    this.lengthPosition = lengthPosition;
    this.lengthDigits = lengthDigits;
    this.exclusiveHeader = exclusiveHeader;
  }

  /**
   * Sets the optional start and/or end delimiter sequence(s) that are expected
   * when receiving a telegram and are added before sending and removed after
   * the telegram was received and only the data is forwarded.
   * This behaviour may be changed by setting the included flags. If these flags
   * are set, the send telegram must already contain the sequence(s) and the
   * forwarded data also contains the sequence(s).
   *
   * @param startCharacters Character sequence at begin of telegram
   * @param endCharacters   Character sequence at end of telegram
   * @param startIncluded   Flag, whether start sequence is included in data
   * @param endIncluded     Flag, whether end sequence is included in data
   */
  public void setDelimiters(String startCharacters, boolean startIncluded, String endCharacters, boolean endIncluded) {
    if (startCharacters != null) {
      this.startDelimiter = new Delimiter(startCharacters, startIncluded);
    }
    if (endCharacters != null) {
      this.endDelimiter = new Delimiter(endCharacters, endIncluded);
    }
  }

  /**
   * Reads from the given input stream until the character sequence of the given
   * delimiter is read fully and returns the read data(including delimiter
   * character sequence).
   *
   * @param in        Input stream
   * @param delimiter Delimiter including character sequence
   * @return Read data
   * @throws IOException
   */
  private String readDelimiter(DataInputStream in, Delimiter delimiter) throws IOException {
    int value = in.read();
    StringBuffer buffer = new StringBuffer();

    while (delimiter != null && value >= 0 &&
      buffer.append((char) value).toString().indexOf(delimiter.characters) < 0) {
      value = in.read();
    }

    if (buffer.length() > 0) {
      return buffer.toString();
    } else {
      return null;
    }
  }

  /**
   * Converts all non-printable characters in the given character sequence. The
   * non-printable characters have to be noted in brackets (eg. [2] for STX).
   *
   * @param characters Character sequence with non-printable characters in brackets
   * @return Character sequence with non-printable characters converted to characters
   */
  public static String convertCharacters(String characters) {
    int position = characters.indexOf("[");
    while (position >= 0) {
      int next = characters.indexOf("]");
      try {
        int value = Integer.parseInt(characters.substring(position + 1, next));
        characters = characters.substring(0, position) + (char) value + characters.subSequence(next + 1, characters.length());
      } catch (NumberFormatException ex) {
        ConnectorService.error("ASCIIProtocol.Delimiter", ex);
      }
      position = characters.indexOf("[");
    }
    return characters;
  }

  /**
   * Data structure for delimiter characters
   */
  private class Delimiter {
    String characters; // character sequence
    boolean included; // true, if character sequence should be included in data

    public Delimiter(String characters, boolean included) {
      this.characters = convertCharacters(characters);
      this.included = included;
    }
  }
}
